<?php
use db\Database;
use db\DatabaseConfig;
use db\QueryResult;

/**
 * It has a set of static functions that let you query for data, make inserts etc.
 * You need to set the database before you use them.
 *
 * @author Jakub Dykowski
 *        
 */
final class Db {
	/**
	 *
	 * @var Database
	 */
	private static $db;
	private static $txnEnabled = true;

	private function __construct() {}

	/**
	 *
	 * @return db\Database or null
	 */
	static function getDatabase() {
		return self::$db;
	}

	/**
	 *
	 * @param db\Database $database        	
	 * @throws InvalidArgumentException
	 */
	static function setDatabase(Database $database) {
		self::$db = $database;
	}

	static function close() {
		return self::$db->close();
	}

	/**
	 * Executes given query (select, insert, update etc.).
	 * Works like sprintf(), but values are escaped.
	 * If you want to check affected rows of insert/update just call count() method of the result
	 *
	 * @param string $query        	
	 * @param
	 *        	mixed varargs values
	 * @return db\QueryResult the result
	 * @throws Exception if query fails
	 */
	static function query($query) {
		$args = func_get_args();
		for($i = 1; $i < count($args); $i++)
			$args[$i] = self::$db->escapeString($args[$i]);
		if (count($args > 1)) {
			$query = call_user_func_array('sprintf', $args);
			if ($query === false)
				throw new InvalidArgumentException("query string format invalid (printf-like) or invalid arguments count");
		}
		return self::$db->run($query);
	}

	/**
	 * Shorthand for query("SELECT $query")
	 *
	 * @param string $query        	
	 * @return db\QueryResult the result
	 * @throws Exception if query fails
	 */
	static function select($query) {
		$args = func_get_args();
		$args[0] = "SELECT $query";
		return call_user_func_array(get_class() . '::query', $args);
	}

	/**
	 * Inserts given array or object to database.
	 * Escapes field names and values (except $table).
	 *
	 * @param multiple:array,object $entity
	 *        	object or array to insert into database
	 * @param string $table
	 *        	table to insert into
	 * @return newly inserted row id
	 * @throws InvalidArgumentException if $entity is neither array nor object
	 * @throws Exception if insert fails
	 */
	static function insert($entity, $table) {
		if (is_object($entity))
			$entity = (array) $entity;
		else if (!is_array($entity))
			throw new InvalidArgumentException("entity has to be object or array");
		if (!count($array))
			throw new InvalidArgumentException("entity has no properties");
		
		foreach ($entity as $key => $value) {
			if ($value === null) {
				unset($entity[$key]);
				continue;
			}
			$columns[] = self::$db->escapeString($key);
			$values[] = $value !== null ? "'".self::$db->escapeString($value)."'" : 'NULL';
		}
		
		$insert = "INSERT INTO `".self::$db->escapeString($table)."` (`" . implode('`,`', $columns) . "`) VALUES(" . implode(",", $values) . ")";
		echo "insert: $insert<br>";
		$result = self::query($insert);
		return $result->id();
	}

	/**
	 * Updates database row based on given array/object.
	 * Escapes field names and values (except $table and $idField).
	 *
	 * @param multiple:array,object $entity        	
	 * @param string $table        	
	 * @param string $column        	
	 * @return bool whether update succeded
	 * @throws InvalidArgumentException
	 */
	static function update($entity, $table, $column) {
		if (is_object($entity))
			$entity = (array) $entity;
		else if (!is_array($entity))
			throw new InvalidArgumentException("entity has to be object or array");
		if ($column == null)
			throw new InvalidArgumentException("column cannot be empty");		
		
		foreach ($entity as $field => $value) {
			if ($field == $column)
				continue;
			$fields[] .= '`' . self::$db->escapeString($field) . '`' . "=" . ($value !== null ? "'" . self::$db->escapeString($value) . "'" : 'NULL') . "";
		}
		
		$update = "UPDATE `".self::$db->escapeString($table)."` SET " . implode(",", $fields) . " WHERE `".self::$db->escapeString($column)."`='" . self::$db->escapeString($entity[$column]) . "' LIMIT 1";
		echo "update: $update<br>";
		return (bool) self::query($update)->count();
	}

	/**
	 * A shorthand for querying row by id or any other unique column.
	 * Escapes column value.
	 *
	 * @param string $table
	 *        	sql table name
	 * @param string $column
	 *        	unique column name
	 * @param string $value
	 *        	column value
	 * @return \ArrayObject database row
	 * @throws \InvalidArgumentException if there is more than 1 result
	 */
	static function get($table, $column, $value) {
		$result = self::query("SELECT * FROM `%s` WHERE `%s`='%s'", $table, $column, $value);
		if ($result->count() > 1) {
			throw new InvalidArgumentException("db query returned more than ONE result, column '$column' needs to be unique");
		}
		return $result->one();
	}

	/**
	 * Same as self::get() but throws exception if not found.
	 *
	 * @param string $table
	 *        	sql table name
	 * @param string $column
	 *        	unique column name
	 * @param string $value
	 *        	column value
	 * @return \ArrayObject loaded table row
	 * @throws \Exception if not found
	 * @throws \InvalidArgumentException if there is more than 1 result
	 */
	static function load($table, $column, $value) {
		$result = self::get($table, $column, $value);
		if ($result == null)
			throw new Exception("row with `$column` == `$value` not found in table `$table`");
		return $result;
	}

	/**
	 * Checks whether given row exists.
	 *
	 * @param string $table
	 *        	sql table name
	 * @param string $column
	 *        	unique column name
	 * @param mixed $entityOrValue
	 *        	column value or entity with that column value
	 * @throws \InvalidArgumentException
	 * @return boolean
	 */
	static function exists($table, $column, $entityOrId) {
		if (is_object($entityOrId)) {
			$fetched = self::get($table, $column, $entityOrId->$column);
		} else if (is_array($entityOrId)) {
			$fetched = self::get($table, $column, $entityOrId[$column]);
		} else if (is_string($entityOrId) || is_numeric($entityOrId)) {
			$fetched = self::get($table, $column, $entityOrId);
		} else
			throw new InvalidArgumentException("entityOrId has to be an array or object or string or number");
		
		return $fetched !== null;
	}

	/**
	 * Begins a transaction.
	 *
	 * @throws \Exception if transactions are not enabled or operation fails
	 */
	static function beginTransaction() {
		if (!self::$txnEnabled)
			throw new Exception("transactins are disabled");
		if (!self::$db->beginTransaction())
			throw new Exception("couldn't begin transaction");
	}

	/**
	 * Rolls back current transaction.
	 *
	 * @throws \Exception if transactions are not enabled or operation fails
	 */
	static function rollback() {
		if (!self::$txnEnabled)
			throw new Exception("transactins are disabled");
		if (!self::$db->rollback())
			throw new Exception("couldn't rollback transaction");
	}

	/**
	 * Commits current transaction.
	 *
	 * @throws \Exception if transactions are not enabled or operation fails
	 */
	static function commit() {
		if (!self::$txnEnabled)
			throw new Exception("transactins are disabled");
		if (!self::$db->commit())
			throw new Exception("couldn't commit transaction");
	}

	/**
	 * Escapes given string.
	 *
	 * @param stream $string        	
	 */
	static function escapeString($string) {
		return self::$db->escapeString($string);
	}
}